// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using CIP4.Common;

namespace CIP4.RulesEngine
{
    public class StringExpressionType : ExpressionType
    {
        public static StringOperatorsEnum StringOperatorsEnumFromString(string value)
        {
            if (string.Compare(value, "CONCAT", true) == 0)
                return StringOperatorsEnum.CONCAT;
            else if (string.Compare(value, "REPLACE", true) == 0)
                return StringOperatorsEnum.REPLACE;
            else if (string.Compare(value, "SUBSTRING", true) == 0)
                return StringOperatorsEnum.SUBSTRING;
            else
                throw new ArgumentException(string.Format("{0} is not a valid StringOperatorsEnum value", value));
        }

        StringOperatorsEnum m_ExpressionOperator;

        public StringOperatorsEnum ExpressionOperator
        {
            get { return m_ExpressionOperator; }
            set { m_ExpressionOperator = value; }
        }

        public override CIP4PrimitiveType UnderlyingType
        {
            get { return CIP4PrimitiveType.CIP4String; }
        }

        public StringExpressionType(StringOperatorsEnum expressionOperator, List<CIValueType> expressionOperands)
        {
            m_ExpressionOperator = expressionOperator;
            Operands.SetOperands(expressionOperands);
        }

        public StringExpressionType(XElement element)
            : base(element)
        {
            XAttribute operatorAtt = element.Attribute("Operator");
            if (operatorAtt != null)
                ExpressionOperator = StringOperatorsEnumFromString(operatorAtt.Value);
        }

        public override void XmlFor(XmlWriter xwrtr, bool writeNamespace)
        {
            xwrtr.WriteStartElement("StringExpression");
            if (writeNamespace)
                xwrtr.WriteAttributeString("xmlns", RuleXmlNS);
            writeNamespace = false;
            xwrtr.WriteAttributeString("Operator", ExpressionOperator.ToString());
            foreach (CIValueType val in Operands)
                val.XmlFor(xwrtr, writeNamespace);
            xwrtr.WriteEndElement();
        }

        public override object Evaluate(IDataPacketType data)
        {
            if (data != null && data.PacketStats != null)
                data.PacketStats.StringOperatorsCount[(int)ExpressionOperator] += 1;
            switch (ExpressionOperator)
            {
                case StringOperatorsEnum.CONCAT:
                    {
                        object op0 = Operands[0].Evaluate(data);
                        object op1 = Operands[1].Evaluate(data);
                        return (op0 == null? string.Empty: op0.ToString()) + (op1 == null? string.Empty: op1.ToString());
                    }
                case StringOperatorsEnum.REPLACE:
                    {
                        object op0 = Operands[0].Evaluate(data);
                        object op1 = Operands[1].Evaluate(data);
                        object op2 = Operands[2].Evaluate(data);
                        if (op0 == null)
                            return null;
                        return (op0.ToString()).Replace(op1 == null? string.Empty: op1.ToString(),  op2 == null? string.Empty : op2.ToString());
                    }
                case StringOperatorsEnum.SUBSTRING:
                    {
                        object op0 = Operands[0].Evaluate(data);
                        object op1 = Operands[1].Evaluate(data);
                        object op2 = Operands.Count > 2? Operands[2].Evaluate(data) : null;
                        if (op0 == null)
                            return null;
                        if (op2 == null)
                            return (op0.ToString()).Substring((int)(long)op1);
                        else
                            return (op0.ToString()).Substring((int)(long)op1, (int)(long)op2);
                    }
                case StringOperatorsEnum.REMOVEHTML:
                    return removeHtml(Operands[0].Evaluate(data).ToString());
                case StringOperatorsEnum.GETREGEXMATCH:
                    {
                        object op0 = Operands[0].Evaluate(data);
                        object op1 = Operands[1].Evaluate(data);
                        return getRegexMatch(op0 == null? string.Empty : op0.ToString(), op1 == null? string.Empty : op1.ToString());
                    }
                default:
                    throw new ApplicationException("unexpected string operator type encountered: working on " + ToString());
            }
        }

        public override bool Check(IDataPacketType data)
        {
            bool result = true;

            LoggerDelegate localLogger = delegate(MajorEnum status, MinorEnum minorStatus, string message, object[] paras)
            {
                if (status == MajorEnum.Error)
                    result = false;
                return data.Logger(status, minorStatus, message, paras);
            };

            switch (ExpressionOperator)
            {
                case StringOperatorsEnum.CONCAT:
                case StringOperatorsEnum.REPLACE:
                    {
                        if (Operands.Count > 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A string expression using the {0} operator cannot have more than two operands: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (Operands.Count < 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A string expression using the {0} operator must have exactly two operands: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (!((Operands[0] is StringAtomType || Operands[0] is StringExpressionType) &&
                            (Operands[1] is StringAtomType || Operands[1] is StringExpressionType)))
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "The operands for the {0} operator must both be strings: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else
                        {
                            Operands[0].Check(data);
                            Operands[1].Check(data);
                        }
                        break;
                    }
                case StringOperatorsEnum.SUBSTRING:
                    {
                        if (Operands.Count > 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "A string expression using the {0} operator cannot have more than two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (Operands.Count < 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "A string expression using the {0} operator must have exactly two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (!((Operands[0] is StringAtomType || Operands[0] is StringExpressionType) &&
                            (Operands[1] is IntegerAtomType || Operands[1] is ArithmeticExpressionType)))
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "The first operand for the {0} operator must be a string, the second operand must be an arthmetic expression or an Integer: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else
                        {
                            Operands[0].Check(data);
                            Operands[1].Check(data);
                        }
                        break;
                    }
                case StringOperatorsEnum.REMOVEHTML:
                    {
                        if (Operands.Count != 1)
                        {
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A string expression using the {0} operator must have exactly one operand: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        }
                        else if (!(Operands[0] is StringAtomType))
                        {
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "The operand for the {0} operator must be a string: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        }
                        break;
                    }
                case StringOperatorsEnum.GETREGEXMATCH:
                    {
                        if (Operands.Count != 2)
                        {
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "A string expression using the {0} operator must have exactly two operands: {1}",
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        }
                        else if (!((Operands[0] is StringAtomType) && Operands[1] is StringAtomType))
                        {
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                                "Both operands for the {0} operator must be a string: {1}", 
                                new object[] { ExpressionOperator.ToString(), this.ToString() });
                        }

                        break;
                    }
                default:
                    {
                        localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, 
                            "{0} is not a recognized string operator: {1}", 
                            new object[] { ExpressionOperator.ToString(), this.ToString() });
                        break;
                    }
            }
            return result;
        }

        private static string removeHtml(string originalString)
        {
            string result;

            Regex r = new Regex("</?\\w+((\\s+\\w+(\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)+\\s*|\\s*)/?>");
            result = r.Replace(originalString, string.Empty);

            return result;
        }

        private static string getRegexMatch(string originalString, string expression)
        {
            string result = null;

            Regex r = new Regex(expression);
            Match m = r.Match(originalString);

            if (m.Success)
            {
                result = m.Value;
            }

            return result;
        }

        public override string ToString(ExpressionFormatEnum format)
        {
            switch (format)
            {
                case ExpressionFormatEnum.javascript:
                    return ToJavaScriptString();
                default:
                    return ToString();
            }
        }

        public override string ToString()
        {
            string result = "";
            string sep = "";
            switch (ExpressionOperator)
            {
                case StringOperatorsEnum.CONCAT:
                    string leftOp = Operands.Count > 0 ? Operands[0].ToString() : "InvalidOperand";
                    string rightOp = Operands.Count > 1 ? Operands[1].ToString() : "InvalidOperand";
                    return "(" + leftOp + ") + (" + rightOp + ")";
                case StringOperatorsEnum.GETREGEXMATCH:
                    result = "Call REGEXMATCH(";
                    foreach (CIValueType value in Operands)
                    {
                        result += sep + value.ToString();
                        sep = ", ";
                    }
                    return result + ")";
                case StringOperatorsEnum.REMOVEHTML:
                    result = "Call REGEXMATCH(";
                    foreach (CIValueType value in Operands)
                    {
                        result += sep + value.ToString();
                        sep = ", ";
                    }
                    return result + ")";
                case StringOperatorsEnum.REPLACE:
                    result = "REPLACE(";
                    foreach (CIValueType value in Operands)
                    {
                        result += sep + value.ToString();
                        sep = ", ";
                    }
                    return result + ")";
                case StringOperatorsEnum.SUBSTRING:
                    result = "MID(";
                    foreach (CIValueType value in Operands)
                    {
                        result += sep + value.ToString();
                        sep = ", ";
                    }
                    return result + ")";
                default:
                    return "InvalidOperator";
            }
        }

        private string ToJavaScriptString()
        {
            string leftOp = Operands[0].ToString(ExpressionFormatEnum.javascript);
            string rightOp = Operands[1].ToString(ExpressionFormatEnum.javascript);
            string thirdOp = Operands.Count > 2 ? Operands[2].ToString(ExpressionFormatEnum.javascript) : "InvalidOPerand";
            switch (ExpressionOperator)
            {
                case StringOperatorsEnum.CONCAT:
                    if (Operands.Count != 2)
                        throw new InvalidExpressionException(Operands.Count + "operands for 'CONCAT' StringExpressionType.");
                    return "(" + leftOp + ")+(" + rightOp + ")";
                case StringOperatorsEnum.GETREGEXMATCH:
                    if (Operands.Count != 2)
                        throw new InvalidExpressionException(Operands.Count + "operands for 'GETREGEXMATCH' StringExpressionType.");
                    return "new RegExp(" + rightOp + ").match(" + leftOp + ")[0]";
                case StringOperatorsEnum.REPLACE:
                    if (Operands.Count != 3)
                        throw new InvalidExpressionException(Operands.Count + "operands for 'REPLACE' StringExpressionType.");
                    return "(" + leftOp + ").replace(new RegExp(" + rightOp + ",'g')," + thirdOp + ")";
                case StringOperatorsEnum.SUBSTRING:
                    switch (Operands.Count)
                    {
                        case 2:
                            return "(" + leftOp + ").substring(" + rightOp + ")";
                        case 3:
                            // TODO: long output, double sending rightOp
                            return "(" + leftOp + ").substring(" + rightOp + ",(" + rightOp + ")-(" + thirdOp + "))";
                        default:
                            throw new InvalidExpressionException(Operands.Count + "operands for 'SUBSTRING' StringExpressionType.");
                    }
                default:
                    throw new InvalidExpressionException("Unhandled operator: " + ExpressionOperator);
            }
        }
    }
}
